package bancosys.tec.datadigester;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jmine.tec.datadigester.DataDigesterMessages;
import jmine.tec.executionlog.ExecutionLogController;
import jmine.tec.executionlog.domain.ExecutionLog;
import bancosys.tec.datadigester.decompressor.Decompressor;
import bancosys.tec.datadigester.decompressor.impl.NoOpDecompressor;
import bancosys.tec.datadigester.input.ResetableInputStreamFactory;
import bancosys.tec.datadigester.processor.Processor;
import bancosys.tec.datadigester.processor.ProcessorFactory;
import bancosys.tec.datadigester.registry.DriverRegistry;
import bancosys.tec.datadigester.registry.DriverRegistryLoader;
import bancosys.tec.persist.controller.AbstractPersistenceEnabledController;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.persist.traverse.visitor.UpperCaseVisitor;
import bancosys.tec.utils.date.Date;

/**
 * Controller do componente. Está configurado, via spring, para ter um proxy em volta.
 */
public class DataDigesterController extends AbstractPersistenceEnabledController {

    /**
     * Registro de drivers. Injetado.
     */
    private DriverRegistry driverRegistry;

    private final DriverRegistryLoader driverLoader;

    /**
     * Responsável pela construção de processors. Injetado.
     */
    private final ProcessorFactory processorFactory;

    private final ExecutionLogController executionLogController;

    private UpperCaseVisitor upperCaseVisitor;

    private final Object lock = new Object();

    /**
     * C'tor para o proxy.
     */
    protected DataDigesterController() {
        this.driverLoader = null;
        this.processorFactory = null;
        this.executionLogController = null;
        this.driverRegistry = null;
    }

    /**
     * Constroi a instância do controller.
     * 
     * @param driverRegistryLoader carregador do registro.
     * @param processorFactory criador de processors.
     * @param executionLogController executionLogController
     */
    public DataDigesterController(DriverRegistryLoader driverRegistryLoader, ProcessorFactory processorFactory,
            ExecutionLogController executionLogController) {
        this.driverLoader = driverRegistryLoader;
        this.processorFactory = processorFactory;
        this.executionLogController = executionLogController;
    }

    /**
     * Descompacta o arquivo, e chama o driver para montar o bean.
     * 
     * @param fileCode código do arquivo.
     * @param fileName name of the file being processed
     * @param inputStream arquivo físico.
     * @return bean obtido a partir do arquivo.
     */
    public DigesterBean digest(String fileCode, String fileName, InputStream inputStream) {
        return this.digest(fileCode, fileName, inputStream, new NoOpDecompressor());
    }

    /**
     * Descompacta o arquivo, e chama o driver para montar o bean.
     * 
     * @param fileCode código do arquivo.
     * @param fileName name of the file being processed
     * @param inputStream inputStream do arquivo a ser lido, DEVE suportar o método reset()
     * @param decompressor decompressor que deve ser utilizado
     * @return bean obtido a partir do arquivo.
     */
    public DigesterBean digest(String fileCode, String fileName, InputStream inputStream, Decompressor decompressor) {
        InputStream preparedInputStream = ResetableInputStreamFactory.createResetableInputStream(inputStream);
        preparedInputStream.mark(Integer.MAX_VALUE);
        return this.doDigest(fileCode, fileName, preparedInputStream, decompressor);
    }

    /**
     * Processa arquivo passado.
     * 
     * @param fileCode tipo de arquivo.
     * @param fileName nome do arquivo.
     * @param inputStream inputStream com dados do arquivo.
     * @throws IOException caso de erro.
     */
    public void process(String fileCode, String fileName, InputStream inputStream) throws IOException {
        this.process(fileCode, fileName, null, null, inputStream, new NoOpDecompressor(), null);
    }

    /**
     * Processa arquivo passado.
     * 
     * @param fileCode tipo de arquivo.
     * @param fileName nome do arquivo.
     * @param inputStream inputStream com dados do arquivo.
     * @param decompressor decompressor que deve ser utilizado
     */
    public void process(String fileCode, String fileName, InputStream inputStream, Decompressor decompressor) {
        this.process(fileCode, fileName, null, null, inputStream, decompressor, null);
    }

    /**
     * Processa arquivo passado.
     * 
     * @param fileCode tipo de arquivo.
     * @param fileName nome do arquivo.
     * @param inputStream array de bytes com dados do arquivo.
     * @param executionId id de execucao.
     * @throws IOException caso algo de errado ocorra ao trabalhar com arquivo.
     */
    public void process(String fileCode, String fileName, InputStream inputStream, Long executionId) throws IOException {
        this.process(fileCode, fileName, null, null, inputStream, new NoOpDecompressor(), executionId);
    }

    /**
     * Processa arquivo passado.
     * 
     * @param fileCode tipo de arquivo.
     * @param fileName nome do arquivo.
     * @param inputStream array de bytes com dados do arquivo.
     * @param decompressor decompressor que deve ser utilizado
     * @param executionId id de execucao.
     */
    public void process(String fileCode, String fileName, InputStream inputStream, Decompressor decompressor, Long executionId) {
        this.process(fileCode, fileName, null, null, inputStream, decompressor, executionId);
    }

    /**
     * Processa arquivo passado.
     * 
     * @param fileCode tipo de arquivo.
     * @param fileName nome do arquivo.
     * @param origem interface que inciou a execução.
     * @param inputStream array de bytes com dados do arquivo.
     * @param decompressor decompressor que deve ser utilizado
     */
    public void process(String fileCode, String fileName, String origem, InputStream inputStream, Decompressor decompressor) {
        this.process(fileCode, fileName, null, origem, inputStream, decompressor, null);
    }

    /**
     * Processa arquivo passado.
     * 
     * @param fileCode tipo de arquivo.
     * @param fileName nome do arquivo.
     * @param execDate data que se deseja usar para os dados do arquivo.
     * @param inputStream array de bytes com dados do arquivo.
     * @param executionId id de execucao.
     */
    public void process(String fileCode, String fileName, Date execDate, InputStream inputStream, Long executionId) {
        this.process(fileCode, fileName, execDate, null, inputStream, new NoOpDecompressor(), executionId);
    }

    /**
     * Processa arquivo passado.
     * 
     * @param fileCode tipo de arquivo.
     * @param fileName nome do arquivo.
     * @param execDate data que se deseja usar para os dados do arquivo.
     * @param origem interface que inciou a execução.
     * @param inputStream inputStream com dados do arquivo.
     * @param decompressor decompressor que deve ser utilizado
     * @param executionId id de execucao.
     */
    public void process(String fileCode, String fileName, Date execDate, String origem, InputStream inputStream, Decompressor decompressor,
            Long executionId) {
        ExecutionLog execution = null;

        InputStream toCleanup = null;

        if (executionId != null) {
            try {
                execution = this.executionLogController.startExistentExecution(executionId);
            } catch (BeanNotFoundException e) {
                throw new IllegalStateException(e);
            }
        } else {
            InputStream preparedInputStream = ResetableInputStreamFactory.createResetableInputStream(inputStream);
            toCleanup = preparedInputStream;
            Date date = (execDate == null) ? new Date() : execDate;
            execution = this.executionLogController.startExecution(fileCode, fileName, date, origem, preparedInputStream);
        }

        try {
            this.doProcess(execution.getFileCode(), execution.getFileName(), execDate, execution.getContent(), decompressor);
        } catch (Throwable e) {
            this.handleThrowable(execution, e);
            throw new DataDigesterException(DataDigesterMessages.GENERIC_DATA_DIGESTER_ERROR_WITH_FILE_NAME.create(fileName), e);
        } finally {
            this.executionLogController.executionFinished();
            if (toCleanup != null) {
                ResetableInputStreamFactory.cleanup(toCleanup);
            }
        }
    }

    /**
     * Verifica se já existem um ExecutionLogItem com erro para o ExecutionLog passado, caso não existir, cria um ExecutionLogItem com a
     * exception passada.
     * 
     * @param execution ExecutionLog
     * @param e erro lançado pelo ExecutionLog
     */
    private void handleThrowable(ExecutionLog execution, Throwable e) {
        if (!this.executionLogController.hasUnsavedErrors()) {
            this.executionLogController.addErrorItem("<service name=\"Erro\" action=\"INVALIDA\" />", e.getMessage(), e);
        }
    }

    /**
     * Processes the file.
     * 
     * @param fileCode fileCode
     * @param fileName fileName
     * @param execDate data que se deseja usar para os dados do arquivo.
     * @param inputStream inputStream
     * @param decompressor decompressor
     */
    private void doProcess(String fileCode, String fileName, Date execDate, InputStream inputStream, Decompressor decompressor) {
        DigesterBean digesterBean = this.doDigest(fileCode, fileName, inputStream, decompressor);

        this.executionLogController.setTotalItemsInExecution(digesterBean.getNumberOfItems());

        Processor processor = this.processorFactory.createProcessor(fileCode);
        if (processor == null) {
            throw new DataDigesterException(DataDigesterMessages.NO_PROCESSOR_FOR_CODE.create(fileCode));
        }
        processor.process(digesterBean);
    }

    /**
     * Digests the file
     * 
     * @param fileCode fileCode
     * @param fileName fileName
     * @param inputStream inputStream
     * @param decompressor decompressor
     * @return DigesterBean
     */
    private DigesterBean doDigest(String fileCode, String fileName, InputStream inputStream, Decompressor decompressor) {
        InputStream decompressed = decompressor.decompress(inputStream);
        DigesterDriver<? extends DigesterBean> driver = this.getDriverRegistry().retrieve(fileCode);
        return driver.parse(new DefaultDigesterFile(fileName, decompressed), this.getDriverProperties(driver.getClass().getName()));
    }

    /**
     * getter de properties.
     * 
     * @param fullQualifiedName nome do driver.
     * @return mapa de properties.
     */
    private Map<String, String> getDriverProperties(String fullQualifiedName) {
        return driverLoader.driverProperties(fullQualifiedName);
    }

    /**
     * @return os registros de drivers
     */
    private DriverRegistry getDriverRegistry() {
        synchronized (this.lock) {
            if (this.driverRegistry == null) {
                this.driverRegistry = this.driverLoader.load();
            }
            return this.driverRegistry;
        }
    }

    /**
     * Limpa o cache de registro de drivers.
     */
    public void clearDriverRegistry() {
        synchronized (this.lock) {
            this.driverRegistry = null;
        }
    }

    /**
     * Devolve um conjunto com todos os códigos de arquivos registrados.
     * 
     * @return um conjunto com todos os códigos de arquivos registrados.
     */
    public Set<String> getFileCodes() {
        return this.getDriverRegistry().getFileCodes();
    }

    /**
     * @return visitor de uppercase, usado em AbstractProcessor.
     */
    public UpperCaseVisitor getUpperCaseVisitor() {
        return this.upperCaseVisitor;
    }

    /**
     * Define upperCaseVisitor, usado em AbstractProcessor.
     * 
     * @param upperCaseVisitor upperCase visitor.
     */
    public void setUpperCaseVisitor(UpperCaseVisitor upperCaseVisitor) {
        this.upperCaseVisitor = upperCaseVisitor;
    }

    /**
     * Inicia execucao.
     * 
     * @param fileCode codigo do arquivo.
     * @param fileName nome do arquivo.
     * @param content conteudo.
     * @return execucao que foi iniciada.
     */
    public ExecutionLog startExecution(String fileCode, String fileName, InputStream content) {
        return this.executionLogController.startExecution(fileCode, fileName, content);
    }

    /**
     * Inicia execucao.
     * 
     * @param fileCode codigo do arquivo.
     * @param fileName nome do arquivo.
     * @param fileDate a data dos dados do arquivo.
     * @param origem interface que iniciou a execução.
     * @param content conteudo.
     * @return execucao que foi iniciada.
     */
    public ExecutionLog startExecution(String fileCode, String fileName, Date fileDate, String origem, InputStream content) {
        return this.executionLogController.startExecution(fileCode, fileName, fileDate, origem, content);
    }

    /**
     * Processa arquivo passado.
     * 
     * @param fileCode tipo de arquivo.
     * @param fileName nome do arquivo.
     * @param inputStream inputStream com dados do arquivo.
     * @throws IOException caso de erro.
     */
    public void fastProcess(String fileCode, String fileName, InputStream inputStream) throws IOException {
        DigesterBean digesterBean = this.doDigest(fileCode, fileName, inputStream, new NoOpDecompressor());
        Processor processor = this.processorFactory.createProcessor(fileCode);
        if (processor == null) {
            throw new DataDigesterException(DataDigesterMessages.NO_PROCESSOR_FOR_CODE.create(fileCode));
        }
        processor.process(digesterBean);
    }

    /**
     * Returns all avaible processor names
     * 
     * @return List of String
     */
    public List<? extends String> getAvailableProcessors() {
        return this.processorFactory.getAvailableProcessors();
    }

}
